Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 10 章 运动商店:创建 RESTful Web Services

作者:Adam Freeman
翻译:陈广
日期:2019-1-2


Web services 对于向客户端应用程序提供数据非常有用,客户端应用程序是用诸如 Angular 或 React 之类的框架编写的。这些应用程序通常在浏览器中运行,不需要 SportsStore 提供的其他 HTML 内容。相反,这些应用程序使用 HTTP 请求与 ASP.NET Core MVC 应用程序交互,并使用 JavaScript Object Notation(JSON)标准格式接收数据。

在本章中,我将通过添加一个 RESTful Web 服务来完成 SportsStore 应用程序,该服务可以为 Web 客户端提供对应用程序数据的访问。ASP.NET Core MVC 对创建 RESTful Web 服务有很好的支持,但是在使用 Entity Framework Core 时必须注意获得正确的结果。

对于 Web services 的工作方式没有必须遵守的规则,但是最常见的方法是采用 Representational State Transfer(REST)模式。REST 没有权威的规范,对于由什么构成 RESTful Web service 也没有共识,但是有一些公共主题被广泛用于 Web services。

REST 的核心前提 —— 也是唯一有广泛共识的方面 —— 是 Web service 通过 URL 和 HTTP 方法(如 GET 和 POST)的组合来定义 API。HTTP 方法指定操作类型,而 URL 指定操作应用的数据对象或对象。

准备本章

本章我继续使用在第4章中创建并在此后的章节中更新的 SportsStore 项目。在 SportsStore 项目文件夹下运行清单10-1所示的命令以重置数据库。

清单 10-1:重置示例应用程序数据库

dotnet ef database drop --force
dotnet ef database update

使用dotnet run启动应用程序,导航至 http://localhost:5000,单击【Seed Data】按钮,然后单击【Production Seed】。数据库将植入少量产品,如图10-1所示。

图10-1 运行示例应用程序

提示:您可以从本书的 GitHub 存储库下载本章的 SportsStore 项目和其他章节的项目:https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc

创建 Web Service

在接下来的部分中,我将构建一个简单的 Web service,它提供了对 SportsStore 应用程序存储的产品数据的访问。

创建存储库

当向应用程序添加 Web service 时,创建单独的存储库是一个好主意,因为客户端应用程序执行的查询与常规 ASP.NET Core MVC 应用程序的并不相同。对于 SportsStore 的 Web service,我在 Models 文件夹下添加了一个名为 IWebServiceRepository.cs 的文件,并使用它定义清单10-2所示的接口。

清单 10-2:Models 文件夹下的 IWebServiceRepository.cs 文件的内容

namespace SportsStore.Models
{
    public interface IWebServiceRepository
    {
        object GetProduct(long id);
    }
}

我从GetProduct方法开始,它将接受一个主键值,并从数据库返回相应的Product对象。GetProduct方法返回的是object而非Product,这样,我就可以演示如何从 Web service 中呈现 Entity Framework Core 数据。

对于存储库实现类,我将一个名为 WebServiceRepository.cs 的文件添加到 Models 文件夹中,并使用它来定义清单10-3所示的类。

清单 10-3:Models 文件夹下的 WebServiceRepository.cs 文件的内容

using System.Linq;

namespace SportsStore.Models
{
    public class WebServiceRepository : IWebServiceRepository
    {
        private DataContext context;
        public WebServiceRepository(DataContext ctx) => context = ctx;

        public object GetProduct(long id)
        {
            return context.Products.FirstOrDefault(p => p.Id == id);
        }
    }
}

此类实现了GetProduct方法,并通过使用 LINQ FirstOrDefault方法来定位存储于数据库中指定Id值的对象。在创建 web service 时,处理不存在数据的请求是很重要的,这就是我使用了FirstOrDefault方法的原因。

为注册存储库及其实现类,我向Startup类添加了清单10-4所示的语句。

清单 10-4:SportsStore 文件夹下的 Startup.cs 文件,配置存储库

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SportsStore.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace SportsStore
{
    public class Startup
    {
        public Startup(IConfiguration config) => Configuration = config;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddTransient<IRepository, DataRepository>();
            services.AddTransient<ICategoryRepository, CategoryRepository>();
            services.AddTransient<IOrdersRepository, OrdersRepository>();
            services.AddTransient<IWebServiceRepository, WebServiceRepository>();
            string conString = Configuration["ConnectionStrings:DefaultConnection"];
            services.AddDbContext<DataContext>(options =>
                options.UseSqlServer(conString));

            services.AddDistributedSqlServerCache(options => {
                options.ConnectionString = conString;
                options.SchemaName = "dbo";
                options.TableName = "SessionData";
            });
            services.AddSession(options => {
                options.Cookie.Name = "SportsStore.Session";
                options.IdleTimeout = System.TimeSpan.FromHours(48);
                options.Cookie.HttpOnly = false;
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseSession();
            app.UseMvcWithDefaultRoute();
        }
    }
}

创建 API 控制器

ASP.NET Core MVC 使用标准控制器功能向应用程序添加 Web services 是非常容易的。我在 Controllers 文件夹中添加了一个名为 ProductValuesController.cs 的类,并添加了清单10-5所示的代码。

清单 10-5:Controllers 文件夹下的 ProductValuesController.cs 文件的内容

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    [Route("api/products")]
    public class ProductValuesController : Controller
    {
        private IWebServiceRepository repository;
        public ProductValuesController(IWebServiceRepository repo)
            => repository = repo;

        [HttpGet("{id}")]
        public object GetProduct(long id)
        {
            return repository.GetProduct(id) ?? NotFound();
        }
    }
}

这个新控制器类的名称是ProductValuesController,它遵循在名称中包含单词Values的约定,以指示控制器将返回数据给它的客户端,而不是 HTML。Web service 控制器的另一个约定是创建路由模式中用于单独处理数据请求的部分。最常见的方法是为以/api开头的 Web services 创建 URL,然后是 Web service 处理的数据类型名称的复数形式。对于处理Product对象的 Web service ,这意味着应该将 HTTP 请求发送到 /api/products URL,我使用Route特性配置了 /api/products URL,如下所示:

...
[Route("api/products")]
...

控制器当前定义的唯一 action 是GetProduct方法,它根据主键返回单个Product对象,该主键是分配给其Id属性的值。此 action 方法使用HttpGet方法进行修饰,它将允许 ASP.NET Core MVC 使用此 action 来处理 HTTP GET 请求。

...
[HttpGet("{id}")]
public Product GetProduct(long id) {
...

特性的参数扩展了由Route特性定义的 URL 模式,以便在表单/api/products/{id}中的 URL 可以访问GetProduct方法。Web services 的 action 方法返回 .NET 对象,这些对象将自动序列化并发送到客户端。

为了防止 Web service 在请求的对象不存在时序列化空响应,action 方法使用空合并操作符调用NotFound方法,如下所示:

return repository.GetProduct(id) ?? NotFound();

这将返回一个【404 - Not Found】,它向客户端发出无法满足请求的信号。

测试 Web Service

若要测试新的 Web service,请使用dotnet run启动应用程序。打开一个新的 PowerShell 窗口,执行清单10-6所示的命令,向 API 控制器发送一个 HTTP get 请求。

注意:在本章中,我使用 PowerShell 的Invoke-RestMethod命令来模拟来自客户端应用程序的请求。

清单 10-6:测试 Web Service

Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

HTTP 请求被分派到 ProductValues 控制器上的GetProduct方法,该控制器使用Find方法从数据库中检索Product对象。Product对象被序列化为 JSON 格式并返回到浏览器,浏览器将显示它接收到的数据。

{
    "id":1,
    "name":"Kayak",
    "description":"A boat for one person",
    "purchasePrice":200.00,
    "retailPrice":275.00,
    "categoryId":1,
    "category":null
}

注意,category导航属性被设置为null,因为我没有要求 Entity Framework Core 加载Product对象的关联数据。

投射结果以排除空导航属性

发送包含null属性的客户端应用程序数据可能会导致混淆,因为无法知道是否不存在关联数据,或存在关联数据但它只是没有包含在响应中。您对数据模型有足够的了解,从而可以认识到categoryId值表示存在关联数据,但是期望客户端应用程序做出这种区分是有问题的,特别是如果它是由不同的程序员团队开发的。如果不想包含关联数据属性,可以通过使 LINQ 来预测结果,从而避免混淆,在结果中排除外键和导航属性,如清单10-7所示。

清单 10-7:Models 文件夹下的 WebServiceRepository.cs 文件,排除属性

using System.Linq;

namespace SportsStore.Models
{
    public class WebServiceRepository : IWebServiceRepository
    {
        private DataContext context;
        public WebServiceRepository(DataContext ctx) => context = ctx;

        public object GetProduct(long id)
        {
            return context.Products
                .Select(p => new
                {
                    Id = p.Id,
                    Name = p.Name,
                    Description = p.Description,
                    PurchasePrice = p.PurchasePrice,
                    RetailPrice = p.RetailPrice
                })
                .FirstOrDefault(p => p.Id == id);
        }
    }
}

我使用了 LINQ 的Select方法来选择我想要包含在结果中的属性,并使用FirstOrDefault方法来选择指定主键值的对象。使用dotnet run重启应用程序,并使用单独的 PowerShell 窗口执行清单10-8中的命令。

清单 10-8:请求一个产品对象

Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

此请求的结果是以下 JSON 数据,它不包括关联据属性:

{
    "id":1,
    "name":"Kayak",
    "description":"A boat for one person",
    "purchasePrice":200.00,
    "retailPrice":275.00
}

如果您检查由应用程序生成的日志消息,将看到 Entity Framework Core 仅从数据库请求了指定的属性。

...
SELECT TOP(1) [p].[Id], [p].[Name], [p].[Description], [p].[PurchasePrice],
    [p].[RetailPrice]
FROM [Products] AS [p]
WHERE [p].[Id] = @__id_0
...

在 Web Service 响应中包含关联数据

如果您希望在 Web Service 响应中包含关联数据,则需要小心。为了演示这个问题,我更改了存储库使用的查询,以便它使用Include方法来选择与Product对象相关联的Category对象,如清单10-9所示。

清单 10-9:Models 文件夹下的 WebServiceRepository.cs 文件,包含关联数据

using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace SportsStore.Models
{
    public class WebServiceRepository : IWebServiceRepository
    {
        private DataContext context;
        public WebServiceRepository(DataContext ctx) => context = ctx;

        public object GetProduct(long id)
        {
            return context.Products.Include(p => p.Category)
                .FirstOrDefault(p => p.Id == id);
        }
    }
}

要查看此代码隐藏的问题,请使用dotnet run启动应用程序,并使用单独的 PowerShell 窗口执行清单10-10所示的命令。

清单 10-10:请求关联数据

Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

您将看到以下错误消息,而不是 JSON 数据:

Invoke-RestMethod : Unable to read data from the transport connection: The connection was closed.

ASP.NET Core MVC 使用一个叫 Json.NET 的程序包来处理序列化,需要进行配置更改才能显示错误的原因,如清单10-11所示。

清单 10-11:SportsStore 文件夹下的 Startup.cs 文件,更改序列化配置

using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SportsStore.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;

namespace SportsStore
{
    public class Startup
    {
        public Startup(IConfiguration config) => Configuration = config;

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddJsonOptions(opts =>
                opts.SerializerSettings.ReferenceLoopHandling
                    = ReferenceLoopHandling.Serialize);
            services.AddTransient<IRepository, DataRepository>();
            services.AddTransient<ICategoryRepository, CategoryRepository>();
            services.AddTransient<IOrdersRepository, OrdersRepository>();
            services.AddTransient<IWebServiceRepository, WebServiceRepository>();
            string conString = Configuration["ConnectionStrings:DefaultConnection"];
            services.AddDbContext<DataContext>(options =>
                options.UseSqlServer(conString));

            services.AddDistributedSqlServerCache(options => {
                options.ConnectionString = conString;
                options.SchemaName = "dbo";
                options.TableName = "SessionData";
            });
            services.AddSession(options => {
                options.Cookie.Name = "SportsStore.Session";
                options.IdleTimeout = System.TimeSpan.FromHours(48);
                options.Cookie.HttpOnly = false;
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseDeveloperExceptionPage();
            app.UseStatusCodePages();
            app.UseStaticFiles();
            app.UseSession();
            app.UseMvcWithDefaultRoute();
        }
    }
}

使用dotnet run重启应用程序,不要使用 PowerShell,打开一个新的浏览器窗口,并请求 http://localhost:5000/api/products/1 URL。当浏览器请求数据,您将看到应用程序报告以下错误并退出:

...
Process is terminating due to StackOverflowException
...

浏览器显示的内容显示了发生了什么。尽管您请求的 URL 针对的是单个Product对象及其关联Category,但应用程序在崩溃之前发送了大量数据。

...
{"id":1,"name":"Kayak","description":"A boat for one person",
    "purchasePrice":200.00,"retailPrice":275.00,"categoryId":1,
    "category": {"id":1,"name":"Watersports",
        "description":"Make a splash",
        "products":[{"id":1,"name":"Kayak",
            "description":"A boat for one person",
            "purchasePrice":200.00,"retailPrice":275.00,
            "categoryId":1,
            "category":{"id":1,"name":"Watersports",
                "description":"Make a splash",
                "products":[{"id":1,"name":"Kayak","description":"A boat for one
...

有一个无尽的循环,其中Product对象的Category导航属性指向其关联的Category对象,其Products导航属性包括Products对象等等。这是由一个名为 fixing up 的 Entity Framework Core 特性造成的,在该特性中,从数据库接收的对象被用作导航属性的值。我在第14章中详细描述了修复过程,并解释了它何时有用,但是对于 RESTful web services 来说,这个特性会导致问题,因为 JSON 序列化程序只是一直跟踪导航属性,直到应用程序遇到堆栈溢出。清单10-11中的配置更改告诉 JSON 序列化程序,即使它已经序列化了对象,也要保持之后的引用。默认行为是在检测到循环时抛出异常,这就是导致清单10-10中的错误的原因。

避免关联数据中的循环引用

无法禁用修复功能,因此避免关联数据的无尽循环的最佳解决方案是在导航属性到达 JSON 序列化程序以创建动态类型之前将其设置为null,以便结果中不包含空值,这是我在清单10-12中采用的方法。

清单 10-12:Models 文件夹下的 WebServiceRepository.cs 文件,避免无尽循环

using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace SportsStore.Models
{
    public class WebServiceRepository : IWebServiceRepository
    {
        private DataContext context;
        public WebServiceRepository(DataContext ctx) => context = ctx;

        public object GetProduct(long id)
        {
            return context.Products.Include(p => p.Category)
                .Select(p => new
                {
                    Id = p.Id,
                    Name = p.Name,
                    PurchasePrice = p.PurchasePrice,
                    Description = p.Description,
                    RetailPrice = p.RetailPrice,
                    CategoryId = p.CategoryId,
                    Category = new
                    {
                        Id = p.Category.Id,
                        Name = p.Category.Name,
                        Description = p.Category.Description
                    }
                })
                .FirstOrDefault(p => p.Id == id);
        }
    }
}

使用dotnet run启动应用程序,并使用单独的 PowerShell 窗口执行清单10-13所示命令。

清单 10-13:请求关联数据

Invoke-RestMethod http://localhost:5000/api/products/1 -Method GET | ConvertTo-Json

该命令生成以下 JSON 结果,其中包括所有关联数据,但不包含循环引用:

{
    "id":  1,
    "name":  "Kayak",
    "purchasePrice":  200.00,
    "description":  "A boat for one person",
    "retailPrice":  275.00,
    "categoryId":  1,
    "category":  {
                     "id":  1,
                     "name":  "Watersports",
                     "description":  "Make a splash"
                 }
}

查询多个对象

在处理多个对象的查询时,重要的事情是限制发送到客户端应用程序的数据量。如果仅是简单地返回数据库中的所有数据,将提高运行应用程序的成本,甚至可能会使客户端应用程序不堪重负。在清单10-14中,我向 Web service 存储库接口添加了一个方法,它允许客户端为结果和所需对象的数量指定一个开始索引。

清单 10-14:Models 文件夹下的 IWebServiceRepository.cs 文件,添加方法

namespace SportsStore.Models
{
    public interface IWebServiceRepository
    {
        object GetProduct(long id);
        object GetProducts(int skip, int take);
    }
}

在清单10-15中,我在实现类中添加了该方法,并使用了上一节中的技术,以避免关联数据的循环引用。

清单 10-15:Models 文件夹下的 WebServiceRepository.cs 文件,添加方法

using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace SportsStore.Models
{
    public class WebServiceRepository : IWebServiceRepository
    {
        private DataContext context;
        public WebServiceRepository(DataContext ctx) => context = ctx;

        public object GetProduct(long id)
        {
            return context.Products.Include(p => p.Category)
                .Select(p => new
                {
                    Id = p.Id,
                    Name = p.Name,
                    PurchasePrice = p.PurchasePrice,
                    Description = p.Description,
                    RetailPrice = p.RetailPrice,
                    CategoryId = p.CategoryId,
                    Category = new
                    {
                        Id = p.Category.Id,
                        Name = p.Category.Name,
                        Description = p.Category.Description
                    }
                })
                .FirstOrDefault(p => p.Id == id);
        }

        public object GetProducts(int skip, int take)
        {
            return context.Products.Include(p => p.Category)
                .OrderBy(p => p.Id)
                .Skip(skip)
                .Take(take)
                .Select(p => new {
                    Id = p.Id,
                    Name = p.Name,
                    PurchasePrice = p.PurchasePrice,
                    Description = p.Description,
                    RetailPrice = p.RetailPrice,
                    CategoryId = p.CategoryId,
                    Category = new
                    {
                        Id = p.Category.Id,
                        Name = p.Category.Name,
                        Description = p.Category.Description
                    }
                });
        }
    }
}

在清单 10-16中,我向 Web service 控制器添加了一个 action,允许客户端请求多个对象。

清单 10-16:Controllers 文件夹下的 ProductValuesController.cs 文件,添加 action

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    [Route("api/products")]
    public class ProductValuesController : Controller
    {
        private IWebServiceRepository repository;
        public ProductValuesController(IWebServiceRepository repo)
            => repository = repo;

        [HttpGet("{id}")]
        public object GetProduct(long id)
        {
            return repository.GetProduct(id) ?? NotFound();
        }

        [HttpGet]
        public object Products(int skip, int take)
        {
            return repository.GetProducts(skip, take);
        }
    }
}

为测试新的 action 方法,使用dotnet run启动应用程序,并使用单独的 PowerShell 窗口执行清单10-17中的命令。

清单 10-17:查询多个对象

Invoke-RestMethod http://localhost:5000/api/products?skip=2"&"take=2 -Method GET | ConvertTo-Json

HTTP 请求要求 Web service 跳过前两个对象,然后返回下两个对象,这将产生以下结果:

{
    "value":  [
        {
            "id":  3,
            "name":  "Soccer Ball",
            "purchasePrice":  18.00,
            "description":  "FIFA-approved size and weight",
            "retailPrice":  19.50,
            "categoryId":  2,
            "category":  {
                "id":  2,
                "name":  "Soccer",
                "description":  "The World\u0027s Favorite Game"
            }
        },
        {
            "id":  4,
            "name":  "Corner Flags",
            "purchasePrice":  32.50,
            "description":  "Give your playing field a professional touch",
            "retailPrice":  34.95,
            "categoryId":  2,
            "category":  {
                "id":  2,
                "name":  "Soccer",
                "description":  "The World\u0027s Favorite Game"
            }
        }
    ],
    "Count":  2
}

完成 Web Service

使用 Entity Framework Core 数据的 Web service 的复杂性在于响应的序列化,如上一节所述。其他标准数据操作遵循前面章节所示的相同模式。在清单10-18中,我向存储库中添加了方法,以允许存储、更新和删除对象。

清单 10-18:Models 文件夹下的 IWebServiceRepository.cs 文件,添加方法

namespace SportsStore.Models
{
    public interface IWebServiceRepository
    {
        object GetProduct(long id);
        object GetProducts(int skip, int take);
        long StoreProduct(Product product);
        void UpdateProduct(Product product);
        void DeleteProduct(long id);
    }
}

在清单10-19中,我向存储库实现类添加了新的方法。

清单 10-19:Models 文件夹下的 WebServiceRepository.cs 文件,添加方法

using System.Linq;
using Microsoft.EntityFrameworkCore;

namespace SportsStore.Models
{
    public class WebServiceRepository : IWebServiceRepository
    {
        private DataContext context;
        public WebServiceRepository(DataContext ctx) => context = ctx;

        public object GetProduct(long id)
        {
            return context.Products.Include(p => p.Category)
                .Select(p => new
                {
                    Id = p.Id,
                    Name = p.Name,
                    PurchasePrice = p.PurchasePrice,
                    Description = p.Description,
                    RetailPrice = p.RetailPrice,
                    CategoryId = p.CategoryId,
                    Category = new
                    {
                        Id = p.Category.Id,
                        Name = p.Category.Name,
                        Description = p.Category.Description
                    }
                })
                .FirstOrDefault(p => p.Id == id);
        }

        public object GetProducts(int skip, int take)
        {
            return context.Products.Include(p => p.Category)
                .OrderBy(p => p.Id)
                .Skip(skip)
                .Take(take)
                .Select(p => new {
                    Id = p.Id,
                    Name = p.Name,
                    PurchasePrice = p.PurchasePrice,
                    Description = p.Description,
                    RetailPrice = p.RetailPrice,
                    CategoryId = p.CategoryId,
                    Category = new
                    {
                        Id = p.Category.Id,
                        Name = p.Category.Name,
                        Description = p.Category.Description
                    }
                });
        }

        public long StoreProduct(Product product)
        {
            context.Products.Add(product);
            context.SaveChanges();
            return product.Id;
        }

        public void UpdateProduct(Product product)
        {
            context.Products.Update(product);
            context.SaveChanges();
        }

        public void DeleteProduct(long id)
        {
            context.Products.Remove(new Product { Id = id });
            context.SaveChanges();
        }
    }
}

注意,StoreProduct方法返回数据库服务器分配给Product对象的主键值。客户端应用程序通常保留自己的数据模型,确保它们拥有执行后续操作所需的信息,而不需要执行额外的查询是很重要的。

更新控制器

在清单10-20中,我更新了 Web service 控制器,以添加与新存储库方法相对应的 action。

清单 10-20:Controllers 文件夹下的 ProductValuesController.cs 文件,添加 action

using Microsoft.AspNetCore.Mvc;
using SportsStore.Models;

namespace SportsStore.Controllers
{
    [Route("api/products")]
    public class ProductValuesController : Controller
    {
        private IWebServiceRepository repository;
        public ProductValuesController(IWebServiceRepository repo)
            => repository = repo;

        [HttpGet("{id}")]
        public object GetProduct(long id)
        {
            return repository.GetProduct(id) ?? NotFound();
        }

        [HttpGet]
        public object Products(int skip, int take)
        {
            return repository.GetProducts(skip, take);
        }

        [HttpPost]
        public long StoreProduct([FromBody] Product product)
        {
            return repository.StoreProduct(product);
        }

        [HttpPut]
        public void UpdateProduct([FromBody] Product product)
        {
            repository.UpdateProduct(product);
        }

        [HttpDelete("{id}")]
        public void DeleteProduct(long id)
        {
            repository.DeleteProduct(id);
        }
    }
}

要测试存储新对象,请使用dotnet run启动应用程序,并使用单独的 PowerShell 窗口执行清单10-21所示的命令。

清单 10-21:存储新数据

Invoke-RestMethod http://localhost:5000/api/products -Method POST -Body (@{Name="Scuba Mask"; Description="Spy on the Fish"; PurchasePrice=21; RetailPrice=40;CategoryId=1} | ConvertTo-Json) -ContentType "application/json"

这个命令很难输入,但是它向服务器发送一个 HTTP POST 请求,其中包含在数据库中存储Product对象所需的所有属性的值。命令完成后,使用浏览器窗口导航到 http://localhost:5000,您将看到新对象,如图10-2所示。

图10-2 使用 RESTful web service 存储对象

要测试更新功能,请使用 PowerShell 窗口运行清单10-22所示的命令,该命令修改了Kayak产品。

清单 10-22:更改数据

Invoke-RestMethod http://localhost:5000/api/products -Method PUT -Body (@{Id=1;Name="Green Kayak"; Description="A Tiny Boat"; PurchasePrice=200; RetailPrice=300;CategoryId=1} | ConvertTo-Json) -ContentType "application/json"

重新加载浏览器窗口,您将看到Kayak产品的RetailPrice值已更改为 300,其名称现在为Green Kayak

要测试删除数据的能力,请使用 PowerShell 窗口运行清单10-23所示的命令。

清单 10-23:删除一个对象

Invoke-RestMethod http://localhost:5000/api/products/1 -Method DELETE

常见问题及解决办法

Web services 的大多数问题都与我在本章前面描述的序列化问题有关。然而,还有一些不太常见的问题,我在下面对它们进行了描述。

存储或更新对象时的空属性值

如果 MVC 模型绑定器创建对象的某些属性为空值或零值,那么最有可能的原因是您在 action 方法参数中忽略了FromBody特性。默认情况下,值只存放于 URL,当您希望 MVC 框架使用请求的其他部分时,必须显式地选择数据源。

Web Service 请求缓慢

请求缓慢的最常见原因是枚举IQueryable<T>对象并意外触发查询。如果您在 JSON 序列化之前处理数据,这是很容易碰到的,重要的是要记住,IQueryable<T>对象将在枚举的任何地方查询数据库,而不仅仅是在 Razor 视图中查询。

“Cannot Insert Explicit Value for Identity Column” 异常

如果在写 Web service 时收到此异常,则最有可能的原因是客户端在要存储在数据库中的对象中包含了一个主键值。如果您正在写客户端应用程序,则可以确保 HTTP 请求不包含值。如果您的 web service 正在被第三方应用程序使用,那么在请求 Entity Framework Core 存储数据之前,您可以在 action 方法中显式地对主键属性进行归零。

总结

本章我通过添加一个简单的 RESTful web service 来完成了该应用程序,该服务可以用于向客户端应用程序提供数据。我向您展示了使用关联数据可能导致的常见问题,并向您展示了如何通过创建动态类型来避免这些问题。在本书的下一部分,我开始更深入地描述 Entity Framework Core 特性。

;

© 2018 - IOT小分队文章发布系统 v0.3